summaryrefslogtreecommitdiff
path: root/ui/routes/(app)/c/[conversation]/+page.svelte
blob: be977379af53a2da877824b6cedd0ec3023d1c02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<script>
  import { DateTime } from 'luxon';
  import { page } from '$app/state';
  import ChannelMeta from '$lib/components/ChannelMeta.svelte';
  import MessageInput from '$lib/components/MessageInput.svelte';
  import MessageRun from '$lib/components/MessageRun.svelte';
  import Message from '$lib/components/Message.svelte';
  import { runs } from '$lib/runs.js';

  const { data } = $props();
  const { session, outbox } = data;
  let activeConversation;

  const subscription = $derived(session.push.subscription);
  const vapid = $derived(session.push.vapidKey);

  const conversationId = $derived(page.params.conversation);
  const conversation = $derived(
    session.conversations.find((conversation) => conversation.id === conversationId),
  );
  const messages = $derived(
    session.messages.filter((message) => message.conversation === conversationId),
  );
  const unsent = $derived(
    outbox.messages.filter((message) => message.conversation === conversationId),
  );
  const deleted = $derived(outbox.deleted.map((message) => message.messageId));
  const unsentSkeletons = $derived(
    unsent.map((message) => message.toSkeleton($state.snapshot(session.currentUser))),
  );
  const messageRuns = $derived(runs(messages.concat(unsentSkeletons), session.currentUser));

  function inView(parentElement, element) {
    const parRect = parentElement.getBoundingClientRect();
    const parentTop = parRect.top;
    const parentBottom = parRect.bottom;

    const elRect = element.getBoundingClientRect();
    const elementTop = elRect.top;
    const elementBottom = elRect.bottom;

    return parentTop < elementTop && parentBottom > elementBottom;
  }

  function getLastVisibleMessage() {
    if (activeConversation) {
      const childElements = activeConversation.getElementsByClassName('message');
      const lastInView = Array.from(childElements)
        .reverse()
        .find((el) => {
          return inView(activeConversation, el);
        });
      return lastInView;
    }
  }

  function setLastRead() {
    const lastInView = getLastVisibleMessage();
    const at = !!lastInView ? DateTime.fromISO(lastInView.dataset.at) : conversation?.at;
    if (!!at) {
      session.local.updateLastReadAt(conversationId, at);
    }
    navigator.serviceWorker.controller.postMessage({
      type: 'CONVERSATION_READ',
      conversationId,
      at,
    });
  }

  $effect(() => {
    const _ = session.messages;
    setLastRead();
  });

  $effect(() => {
    // This is just to force it to track messageRuns.
    const _ = messageRuns;
    document.querySelector('.message-run:last-child .message:last-child')?.scrollIntoView();
  });

  function handleKeydown(event) {
    if (event.key === 'Escape') {
      setLastRead(); // TODO: pass in "last message DT"?
    }
  }

  let lastReadCallback = null;

  function onscroll() {
    clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still.
    lastReadCallback = setTimeout(setLastRead, 2 * 1000);
  }

  async function subscribe() {
    // TODO: we need to provide specific subscription stuff, right?
    await session.push.subscribe();
  }

  async function sendMessage(message) {
    outbox.sendToConversation(conversationId, message);
  }

  async function deleteMessage(id) {
    outbox.deleteMessage(id);
  }
</script>

<svelte:window onkeydown={handleKeydown} />

<ChannelMeta {subscribe} {vapid} {subscription} />
<div class="active-conversation" {onscroll} bind:this={activeConversation}>
  {#each messageRuns as { sender, ownMessage, messages }}
    <MessageRun
      {sender}
      class={{
        ['own-message']: ownMessage,
        ['other-message']: !ownMessage,
      }}
    >
      {#each messages as message}
        <Message
          {...message}
          editable={ownMessage}
          {deleteMessage}
          class={{
            unsent: !message.id,
            deleted: deleted.includes(message.id),
          }}
        />
      {/each}
    </MessageRun>
  {/each}
</div>
<MessageInput {sendMessage} />